package okw.communication;

import java.net.BindException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import javax.inject.Inject;

import okw.database.DatabaseService;
import okw.database.model.MessageReceived;
import okw.database.model.MessageToSend;
import okw.database.model.Node;
import okw.gui.DlgCommunicationState;
import okw.packager.PackagerService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.google.common.collect.Lists;

import core.communication.configurationNode.AddConfigurationNode;
import core.communication.configurationNode.ConfigurationNode;
import core.communication.configurationNode.DeleteConfigurationNode;
import core.communication.configurationNode.EditConfigurationNode;
import core.communication.configurationNode.SetParentConfigurationNode;
import core.communication.message.CNodeState;
import core.communication.message.CommunicationTestMessage;
import core.communication.message.Direction;
import core.communication.message.Message;
import core.communication.message.MessagePriorityQueue;
import core.communication.message.NodeStateEnum;
import core.communication.message.NodeStateMessage;
import core.utils.Converter;

@Service
public class CommunicationServiceImpl implements CommunicationService {
	private static final Logger L = LoggerFactory.getLogger(CommunicationServiceImpl.class);

	private DatabaseService databaseService;
	private Server server;
	private MessageSender messageSender;
	private MessageReceiver messageReceiver;
	private ClientChecker clientChecker;
	private MessagePriorityQueue messagesToSend = new MessagePriorityQueue();
	private MessagePriorityQueue messagesReceived = new MessagePriorityQueue();
	private LinkedList<Client> clientsToCheck = new LinkedList<Client>();
	private HashMap<String, Client> upperClients = new HashMap<String, Client>();
	private HashMap<String, Client> lowerClients = new HashMap<String, Client>();
	private Node mainNode;
	private ThreadPoolTaskExecutor taskExecutor;
	private PackagerService packagerService;

	@Inject
	public CommunicationServiceImpl(DatabaseService databaseService, Server server, MessageSender messageSender, MessageReceiver messageReceiver, ClientChecker clientChecker,
			ThreadPoolTaskExecutor taskExecutor, PackagerService packagerService) {
		L.debug("Tworzę CommunicationService");

		Assert.notNull(databaseService);
		Assert.notNull(server);
		Assert.notNull(messageSender);
		Assert.notNull(messageReceiver);
		Assert.notNull(clientChecker);
		Assert.notNull(taskExecutor);
		Assert.notNull(packagerService);

		this.databaseService = databaseService;
		this.server = server;
		this.messageSender = messageSender;
		this.messageReceiver = messageReceiver;
		this.clientChecker = clientChecker;
		this.taskExecutor = taskExecutor;
		this.packagerService = packagerService;

		L.debug("Utworzono CommunicationService");
	}

	@Override
	public void setUp() throws BindException {
		L.debug("setUp");

		mainNode = databaseService.getMainNode();
		server.setUp(mainNode.getLocatorURI());

		setUpClients();

		setUpMessages();

		taskExecutor.execute(messageSender);
		taskExecutor.execute(messageReceiver);
		taskExecutor.execute(clientChecker);
	}

	private void setUpMessages() {
		L.debug("setUpMessages");

		List<MessageToSend> messagesTS = databaseService.getMessagesToSend();
		List<MessageReceived> messagesR = databaseService.getMessagesReceived();

		for (MessageToSend messageToSend : messagesTS) {
			Message message = (Message) Converter.xmlToObject(messageToSend.getValue());
			message.setMessageToSendDatabaseId(messageToSend.getId());
			messagesToSend.add(message);
		}

		for (MessageReceived messageReceived : messagesR) {
			Message message = (Message) Converter.xmlToObject(messageReceived.getValue());
			messagesReceived.add(message);
		}
	}

	private void setUpClients() {
		L.debug("setUpClients");

		List<Node> topNodes = databaseService.getNodesForDirection(Direction.UP);
		for (Node node : topNodes)
			upperClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
		List<Node> downNodes = databaseService.getNodesForDirection(Direction.DOWN);
		for (Node node : downNodes)
			lowerClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
	}

	@Override
	public void shutDown() {
		L.debug("shutDown");

		server.shutdownServer();
		messageSender.stop();
		messageReceiver.stop();
		clientChecker.stop();
		packagerService.stopPackagerThread();
	}

	@Override
	public void addReceivedMessage(Message message) {
		L.info("Dodaje otrzymaną wiadomośc {}", message.getMessageType());
		synchronized (messagesReceived) {
			databaseService.saveReceivedMessage(message);
			messagesReceived.add(message);
			messageReceiver.wakeUp();
		}
	}

	@Override
	public Message getReceivedMessage() {
		Message message = null;
		synchronized (messagesReceived) {
			message = messagesReceived.get();
		}
		return message;
	}

	@Override
	public void removeReceivedMessage(Message message) {
		databaseService.setMessageAsHandled(message.getMessageToSendDatabaseId());
		messagesReceived.remove(message);
	}

	@Override
	public void handleConfiguration(List<ConfigurationNode> configurationNodes) {
		L.debug("Obsługuje ConfigureationMessage");
		for (ConfigurationNode configurationNode : configurationNodes) {
			L.debug("ConfigurationNode {}" + configurationNode.getClass().getName());

			if (configurationNode instanceof SetParentConfigurationNode) {
				handleSetParentConfigurationNode(configurationNode);
			} else if (configurationNode instanceof AddConfigurationNode) {
				handleAddConfigurationNode(configurationNode);
			} else if (configurationNode instanceof EditConfigurationNode) {
				handleEditConfigurationNode(configurationNode);
			} else if (configurationNode instanceof DeleteConfigurationNode) {
				handleDeleteConfigurationNode(configurationNode);
			}
		}
	}

	private void handleSetParentConfigurationNode(ConfigurationNode configurationNode) {
		SetParentConfigurationNode conf = (SetParentConfigurationNode) configurationNode;
		mainNode.setPrefferedParentURI(conf.getTargetNodeUri());
		mainNode = databaseService.storeNode(mainNode);
	}

	private void handleAddConfigurationNode(ConfigurationNode configurationNode) {
		AddConfigurationNode conf = (AddConfigurationNode) configurationNode;
		Node node = new Node(conf.getChangedNode(), conf.getDirection());
		node = databaseService.storeNode(node);
		if (conf.getDirection() == Direction.UP)
			upperClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
		else if (conf.getDirection() == Direction.DOWN)
			lowerClients.put(node.getLocatorURI(), new Client(node.getLocatorURI(), node.getNodeType()));
	}

	private void handleEditConfigurationNode(ConfigurationNode configurationNode) {
		EditConfigurationNode conf = (EditConfigurationNode) configurationNode;
		Node newNode = new Node(conf.getChangedNode(), conf.getDirection());

		if (conf.getTargetNodeUri().compareTo(newNode.getLocatorURI()) != 0) {
			Client client = null;
			if (upperClients.containsKey(conf.getTargetNodeUri())) {
				client = upperClients.get(conf.getTargetNodeUri());
				client.disconnect();
				upperClients.remove(conf.getTargetNodeUri());
				upperClients.put(newNode.getLocatorURI(), new Client(newNode.getLocatorURI(), newNode.getNodeType()));
			} else if (lowerClients.containsKey(conf.getTargetNodeUri())) {
				client = lowerClients.get(conf.getTargetNodeUri());
				client.disconnect();
				lowerClients.remove(conf.getTargetNodeUri());
				lowerClients.put(newNode.getLocatorURI(), new Client(newNode.getLocatorURI(), newNode.getNodeType()));
			}
		}

		databaseService.updateNode(conf.getTargetNodeUri(), newNode);
	}

	private void handleDeleteConfigurationNode(ConfigurationNode configurationNode) {
		DeleteConfigurationNode conf = (DeleteConfigurationNode) configurationNode;
		databaseService.deleteNode(conf.getTargetNodeUri());
		deleteClient(conf.getTargetNodeUri());
	}

	private void deleteClient(String targetNodeUri) {
		Client client = upperClients.remove(targetNodeUri);
		if (client == null)
			client = lowerClients.remove(targetNodeUri);
		if (client != null) {
			L.debug("Usuwam klienta " + client.toString());
			removeClientToCheck(client);
			client.disconnect();
		}
	}

	@Override
	public void send(Message message) {
		L.debug("Wysyłam wiadomość {}", message.getMessageType());

		synchronized (messagesToSend) {
			databaseService.storeMessageToSend(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void sendNodeStateMessage(String locatorUri, NodeStateEnum state) {
		L.debug("Wysyłam wiadomość stanu " + locatorUri + " " + state);
		Message message = new NodeStateMessage(locatorUri, new CNodeState(state));
		send(message);
	}

	@Override
	public String getServerURI() {
		return mainNode.getLocatorURI();
	}

	@Override
	public Client getClientToCheck() {
		Client client = null;
		synchronized (clientsToCheck) {
			if (!clientsToCheck.isEmpty())
				client = clientsToCheck.getFirst();
		}
		return client;
	}

	@Override
	public void removeClientToCheck(Client client) {
		L.debug("Usuwam {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.remove(client);
		}
	}

	@Override
	public void reAddClientToCheck(Client client) {
		synchronized (clientChecker) {
			clientsToCheck.remove(client);
			clientsToCheck.add(client);
		}
	}

	@Override
	public Message getMessageToSend() {
		Message message = null;
		synchronized (messagesToSend) {
			message = messagesToSend.get();
		}

		return message;
	}

	@Override
	public HashMap<String, Client> getUpperClients() {
		return upperClients;
	}

	@Override
	public HashMap<String, Client> getLowerClients() {
		return lowerClients;
	}

	@Override
	public void resendMessage(Message message) {
		L.debug("Ponownie wysyłam wiadomość {}", message.getMessageType());
		synchronized (messagesToSend) {
			messagesToSend.remove(message);
			messagesToSend.add(message);
			messageSender.wakeUp();
		}
	}

	@Override
	public void removeMessageToSend(Message message) {
		L.debug("Usuwam wiadomośc {}", message.getMessageType());
		synchronized (messagesToSend) {
			databaseService.removeMessageToSend(message.getMessageToSendDatabaseId());
			messagesToSend.remove(message);
		}
	}

	@Override
	public void addClientToCheck(Client client) {
		L.debug("Dodaje {} do sprawdzenia", client.getLocatorURI());
		synchronized (clientsToCheck) {
			clientsToCheck.add(client);
			clientChecker.wakeUp();
		}
	}

	@Override
	public String getPrefferedParentURI() {
		return mainNode.getPrefferedParentURI();
	}

	@Override
	@Async
	public void checkCommunication(DlgCommunicationState dlgCommunicationState) {
		List<Client> clients = Lists.newArrayList();
		clients.addAll(upperClients.values());
		clients.addAll(lowerClients.values());
		
		Boolean canConnect; 
		
		for (Client client : clients) {
			canConnect = true;
			if (!client.getIsWorking()) {
				canConnect = false;
			} else {
				CommunicationTestMessage msg = new CommunicationTestMessage();
				msg.setDestinationURI(client.getLocatorURI());
				try {
					client.send(msg);
				} catch (Throwable e) {
					client.setIsWorking(false);
					sendNodeStateMessage(client.getLocatorURI(), NodeStateEnum.OFF);
					addClientToCheck(client);
					canConnect = false;
				}
			}
			
			if (dlgCommunicationState != null)
				dlgCommunicationState.updateNodeState(client.getLocatorURI(), canConnect);
			else
				break;
		}
	}
}
